/*
 * Albareth - an action roleplaying game.
 * Copyright (C) 2008 Jens Anuth
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 *   02110-1301, USA.
 * 
 * $Id: DungeonWindow.java 45 2008-05-14 23:04:29Z jens464 $
*/
package albareth.frontend.slick.ui;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import org.newdawn.slick.Color;
import org.newdawn.slick.Font;
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.geom.Vector2f;
import org.newdawn.slick.particles.ConfigurableEmitter;
import org.newdawn.slick.particles.ConfigurableEmitter.SimpleValue;

import albareth.core.GameEffect;
import albareth.core.GameMap;
import albareth.core.entity.Activatable;
import albareth.core.entity.ActionItem;
import albareth.core.entity.Entity;
import albareth.core.entity.Item;
import albareth.core.entity.Mobile;
import albareth.core.entity.PlayerCharacter;
import albareth.frontend.TileCollection;
import albareth.frontend.slick.AbstractSlickGameEffect;
import albareth.frontend.slick.ParticleGameEffect;
import albareth.frontend.slick.SlickFrontend;
import albareth.frontend.slick.SlickResources;
import albareth.frontend.slick.TileGameEffect;
import albareth.util.Location;
import frosch.slick.glider.VectorGlider;
import frosch.util.Coordinates;
import frosch.util.Direction;
import frosch.util.Log;
import frosch.util.glider.Easing;
import frosch.util.glider.PropertyGlider;

public class DungeonWindow extends UIElement
{
    public static final Color OUT_OF_SIGHT_FILTER = new Color(0.24f, 0.24f, 0.24f, 1.0f);
    
    public static final int OVERHEAD_MSG_DECAY_TIME = 2000;
    
    private boolean _scrollMobiles = false; 
    
    public final int MAPTILE_WIDTH;
    public final int MAPTILE_HEIGHT;
    
    private final int BORDER_OFFSET_X = -16; 
    private final int BORDER_OFFSET_Y = -16;
    
    //private GameContainer _container = null;
    SlickFrontend _frontend;

    private int _widthInTiles = 13;
    private int _heightInTiles = 13;
    
    private float _scale = 2.0f;
    
    //current position of mouse cursor
    private int _cursorTileOffsetX = -1;
    private int _cursorTileOffsetY = -1;
    
    //current position of target cursor
    private int _targetTileOffsetX = -1;
    private int _targetTileOffsetY = -1;

    private int _currentMouseWalkDirection = Direction.NOWHERE;
    private boolean _mouseDownForWalking = false;
    
    private List _effects = new LinkedList();
    private List _newEffects = new LinkedList();

    private List _overheadMessages = new LinkedList();

    private boolean _meleeCooldownCursorShown = false;
    private boolean _meleeCursorShown = false;

    
    

    public DungeonWindow(int px, int py,
                         int width, int height,
                         SlickFrontend frontend)
    {
        super(px, py, width, height);
        _frontend = frontend;
        MAPTILE_WIDTH = TileCollection.getDefaultTileWidth();
        MAPTILE_HEIGHT = TileCollection.getDefaultTileHeight();
        setDraggingAllowed(true);
        setDroppingAllowed(true);
    }
    
    public float getScale()
    {
        return _scale;
    }

    public void setScale(float scale)
    {
        _scale = scale;
        //updateComponentSize();
    }
    
    public void setSizeInTiles(int width, int height)
    {
        _widthInTiles = width;
        _heightInTiles = height;
        //updateComponentSize();
    }

    /*
    private void updateComponentSize()
    {
        componentWidth = (int)(_widthInTiles * MAPTILE_WIDTH * _scale);
        componentHeight = (int)(_heightInTiles * MAPTILE_HEIGHT * _scale);
    }*/
    
    public int getTileByPixelX(int pixelX)
    {
        return (int)((pixelX - BORDER_OFFSET_X) / (MAPTILE_WIDTH * _scale));
    }
    
    public int getTileByPixelY(int pixelY)
    {
        return (int)((pixelY - BORDER_OFFSET_Y) / (MAPTILE_HEIGHT * _scale));
    }
    
    public int getMapByPixelX(int pixelX)
    {
        return  getMapByTileX(getTileByPixelX(pixelX));
    }

    public int getMapByPixelY(int pixelY)
    {
        return getMapByTileY(getTileByPixelY(pixelY));
    }

    public int getMapByTileX(int tileX)
    {
        Mobile player = _frontend.getPlayerCharacter();
        return player.getX() + tileX - _widthInTiles/2;
    }
    
    public int getMapByTileY(int tileY)
    {
        Mobile player = _frontend.getPlayerCharacter();
        return player.getY() + tileY - _heightInTiles/2;
    }
    
    public int getTileByMapX(int mapX)
    {
        Mobile player = _frontend.getPlayerCharacter();
        return mapX - player.getX() + _widthInTiles/2;
    }

    public int getTileByMapY(int mapY)
    {
        Mobile player = _frontend.getPlayerCharacter();
        return mapY - player.getY() + _heightInTiles/2;
    }

    
    public Vector2f getWindowRelativePixelLocation(Location location)
    {
        Mobile player = _frontend.getPlayerCharacter();
        
        int tileX = location.x - player.getX() + _widthInTiles/2; 
        int tileY = location.y - player.getY() + _heightInTiles/2;
        return new Vector2f(
                (tileX * MAPTILE_WIDTH + MAPTILE_WIDTH/2) * _scale,
                (tileY * MAPTILE_HEIGHT + MAPTILE_HEIGHT/2) * _scale
        );
    }

    public Vector2f getWindowRelativePixelLocation(Vector2f mapPixelLoc)
    {
        Mobile player = _frontend.getPlayerCharacter();
        Vector2f playerLoc = getMapRelativePixelLocation(player.getLocation());
        
        return new Vector2f(
                mapPixelLoc.x - playerLoc.x + (_widthInTiles * MAPTILE_WIDTH * _scale)/2,
                mapPixelLoc.y - playerLoc.y + (_heightInTiles * MAPTILE_HEIGHT * _scale)/2
        );
    }

    public Vector2f getMapRelativePixelLocation(Location location)
    {
        return new Vector2f(
                (location.x * MAPTILE_WIDTH + MAPTILE_WIDTH/2) * _scale,
                (location.y * MAPTILE_HEIGHT + MAPTILE_HEIGHT/2) * _scale
        );
    }
    
    public Location getLocationByMapRelativePixel(Vector2f mapPixelLoc)
    {
        Mobile player = _frontend.getPlayerCharacter();
        return new Location(
                (int)(mapPixelLoc.x / (MAPTILE_WIDTH * _scale)),
                (int)(mapPixelLoc.y / (MAPTILE_HEIGHT * _scale)),
                player.getLocation().map
        );
    }

    


    public boolean isLocationOnScreen(Location location)
    {
        Mobile _player = _frontend.getPlayerCharacter();
        
        if (location.map != _player.getMap()) {
            return false;
        }
        
        int diffX = _player.getX() - location.x; 
        int diffY = _player.getY() - location.y;
        
        //out of map?
        if (Math.abs(diffX) > _widthInTiles/2 || Math.abs(diffY) > _heightInTiles/2) {
            return false;
        }
        
        return true;
    }
    
    
    
    /*
    public Vector2f getEntityPixelLocation(Entity e)
    {
        float tilewidth = _scale * TileCollection.getDefaultTileWidth();
        float tileheight = _scale * TileCollection.getDefaultTileHeight();
        float x = e.getX() * tilewidth + tilewidth/2;
        float y = e.getY() * tileheight + tileheight/2;
        //TODO: plus map-view x and y offsets
        return new Vector2f(x, y);
    }*/

    
    /**
     * Renders a part of the dungeon map centered on the player,
     * as well as the particle effects and the cursor location.
     */
    public void render(Graphics g, GameContainer container, int screenX, int screenY) throws SlickException
    {
        
        //TODO: use background image and padding instead
        SlickResources.DUNGEON_WIN_BORDER.draw(screenX - 8, screenY - 6);
        g.setClip(screenX, screenY, _componentWidth, _componentHeight);
        
        Mobile player = _frontend.getPlayerCharacter();
        GameMap map = player.getMap();

        int camScrollModX = -getScrollXMod(player);
        int camScrollModY = -getScrollYMod(player);

        int absoluteScreenX = screenX + BORDER_OFFSET_X + camScrollModX;
        int absoluteScreenY = screenY + BORDER_OFFSET_Y + camScrollModY;
        
        List upperLayer = new LinkedList();

        for (int x = 0; x < _widthInTiles; x++)
        {
            for (int y = 0; y < _heightInTiles; y++)
            {
                float screenPosX = absoluteScreenX + MAPTILE_WIDTH * x * _scale;
                float screenPosY = absoluteScreenY + MAPTILE_HEIGHT * y * _scale;

                int mapX = getMapByTileX(x);//player.getX() + x - _widthInTiles/2; 
                int mapY = getMapByTileY(y);//player.getY() + y - _heightInTiles/2;
                
                //out of map or not explored?
                if (!map.isExplored(mapX, mapY) || map.isOutOfBounds(mapX, mapY))
                {
                    SlickFrontend.drawTile(
                            TileCollection.BLACK, screenPosX, screenPosY, _scale, null, false
                    );
                    continue;
                }
                
                boolean hasLoS = player.hasLineOfSight(mapX, mapY, map);
                
                //ground:
                SlickFrontend.drawTile(
                        map.getGround(mapX, mapY).getTile(),
                        screenPosX, screenPosY, _scale,
                        (!hasLoS) ? OUT_OF_SIGHT_FILTER : null,
                        false
                );
                
                //if we don't have los, we only draw the dungeon ground.
                if (!hasLoS) {
                    continue;
                }

                //items:
                if (map.getItems(mapX, mapY) != null)
                {
                    for (Iterator it = map.getItems(mapX, mapY).iterator(); it.hasNext(); )
                    {
                        Item item = (Item)it.next();
                        if (item.isUpperLayer()) {
                            upperLayer.add(item);
                        } else {
                            SlickFrontend.drawTile(
                                    item.getTile(),
                                    screenPosX, screenPosY, _scale, null, false
                            );
                        }
                    }
                }
            }
        }
        
        renderMobiles(g, absoluteScreenX, absoluteScreenY);
        
        //render spell-particles
        for (Iterator it = _effects.iterator(); it.hasNext(); )
        {
            AbstractSlickGameEffect effect = (AbstractSlickGameEffect)it.next();
            effect.render(g, absoluteScreenX, absoluteScreenY);
        }

        
        renderUpperLayer(g, absoluteScreenX, absoluteScreenY, upperLayer);

        
        drawTargetCursor(g, absoluteScreenX, absoluteScreenY);
        drawMouseCursor(g, absoluteScreenX, absoluteScreenY);
        
        renderOverheadMessages(g, absoluteScreenX, absoluteScreenY);
        
        g.setClip(0, 0, SlickFrontend.SCREEN_WIDTH, SlickFrontend.SCREEN_HEIGHT);


    }
    
    
    private void renderOverheadMessages(Graphics g, int screenX, int screenY)
    {
        for (Iterator it = _overheadMessages.iterator(); it.hasNext(); )
        {
            OverheadMessage message = (OverheadMessage)it.next();
            message.render(g, screenX, screenY);
        }
    }

    private void renderUpperLayer(Graphics g, int screenX,
            int screenY, List upperLayer) throws SlickException
    {
        for (Iterator it = upperLayer.iterator(); it.hasNext(); )
        {
            Item item = (Item)it.next();
            int tileX = getTileByMapX(item.getX());
            int tileY = getTileByMapY(item.getY());
            
            float screenPosX = screenX + MAPTILE_WIDTH * tileX * _scale;
            float screenPosY = screenY + MAPTILE_HEIGHT * tileY * _scale;
            
            SlickFrontend.drawTile(
                    item.getTile(),
                    screenPosX, screenPosY, _scale, null, false
            );


        }
    }

    public void renderMobiles(Graphics g, int screenX, int screenY)
        throws SlickException
    {
        PlayerCharacter player = _frontend.getPlayerCharacter(); 
        GameMap map = player.getMap();

        for (int x = 0; x < _widthInTiles; x++)
        {
            for (int y = 0; y < _heightInTiles; y++)
            {
                int mapX = getMapByTileX(x);//player.getX() + x - _widthInTiles/2; 
                int mapY = getMapByTileY(y);//player.getY() + y - _heightInTiles/2;

                if (!player.hasLineOfSight(mapX, mapY, map)) {
                    continue;
                }
                

                Mobile mobile = map.getMobile(mapX, mapY);
                if (mobile != null)
                {
                    float screenPosX = screenX + MAPTILE_WIDTH * x * _scale;
                    float screenPosY = screenY + MAPTILE_HEIGHT * y * _scale;

                    //visually "snap" towards combat-opponent in melee:
                    int transX = 0, transY = 0;
                    boolean isBeating = false;
                    long lastMelee = mobile.getTimeSinceLastMeleeAttack();
                    Mobile opponent =
                        mobile.getMobileController().getTargetMobile();
                    if (lastMelee <= 100 && opponent != null) 
                    {
                        isBeating = true;
                        int diffX = mobile.getX() - opponent.getX();
                        int diffY = mobile.getY() - opponent.getY();

                        float pct = -3.0f * ((float)lastMelee/100);
                        transX = (int)(8 * diffX * pct);
                        transY = (int)(8 * diffY * pct);
                        g.translate(transX, transY);
                    }
                    
                    boolean drawFlash =
                        mobile.getTimeSinceLastDamage() < 60;
                    
                    int scrollXMod = _scrollMobiles ? getScrollXMod(mobile) : 0; 
                    int scrollYMod = _scrollMobiles ? getScrollYMod(mobile) : 0;
                    
                    SlickFrontend.drawTile(
                            mobile.getTile(),
                            scrollXMod + screenPosX,
                            scrollYMod + screenPosY, _scale,
                            SlickFrontend.getMobileFilterColor(mobile),
                            drawFlash
                    );

                    if (isBeating) {
                        g.translate(-transX, -transY);
                    }
                }
            }
        }
    }

    
    private int getScrollXMod(Mobile mobile)
    {
        long walkCooldownLeft = mobile.getWalkCooldownTimeLeft();
        if (walkCooldownLeft > 0)
        {
            return (int)(MAPTILE_WIDTH * _scale * 
                -Direction.getXMod(mobile.getLastWalkDirection()) *
                walkCooldownLeft / mobile.getWalkCooldown());
        }
        return 0;
    }

    private int getScrollYMod(Mobile mobile)
    {
        long walkCooldownLeft = mobile.getWalkCooldownTimeLeft();
        if (walkCooldownLeft > 0)
        {
            return (int)(MAPTILE_HEIGHT * _scale * 
                -Direction.getYMod(mobile.getLastWalkDirection()) *
                walkCooldownLeft / mobile.getWalkCooldown());
        }
        return 0;
    }

    private void drawTargetCursor(Graphics g, int screenX, int screenY) throws SlickException
    {
        if (_targetTileOffsetX > -1)
        {
            SlickFrontend.drawTile(
                    TileCollection.TARGET,
                    screenX + _targetTileOffsetX * MAPTILE_WIDTH * _scale,
                    screenY + _targetTileOffsetY * MAPTILE_HEIGHT * _scale,
                    _scale, null, false
            );
        }
    }

    
    private void drawMouseCursor(Graphics g, int screenX, int screenY) throws SlickException
    {
        if (_cursorTileOffsetX > -1)
        {
            SlickFrontend.drawTile(
                    TileCollection.CURSOR,
                    screenX + _cursorTileOffsetX * MAPTILE_WIDTH * _scale,
                    screenY + _cursorTileOffsetY * MAPTILE_HEIGHT * _scale,
                    _scale, null, false
            );
        }
    }
    

    public boolean update(int delta, GameContainer container) throws SlickException
    {
        if (_mouseDownForWalking) {
            _frontend.playerWalk(_currentMouseWalkDirection);
        }
        
        if (_meleeCooldownCursorShown &&
                _frontend.getPlayerCharacter().getMeleeCooldownTimeLeft() < 0)
        {
            _meleeCooldownCursorShown = false;
            setMouseCursor(_currentMouseWalkDirection, -1);
        }
        
        updateTargetCursor();
        updateParticleEffects(delta);
        updateOverheadMessages(delta);

        return false;
    }


    /**
     * Fades color of overhead messages, eases and finally removes them.
     * TODO: easing effects
     */
    private void updateOverheadMessages(int delta)
    {
        for (Iterator it = _overheadMessages.iterator(); it.hasNext(); )
        {
            OverheadMessage message = (OverheadMessage)it.next();
            message.update(delta);
            if (message.finished) {
                it.remove();
            }

        }
    }

    /**
     * update screen-location of target cursor.
     */
    private void updateTargetCursor()
    {
        Mobile player = _frontend.getPlayerCharacter();

        Mobile targetMob = _frontend.getTargetMobile();
        if (targetMob == null ||
            !isLocationOnScreen(targetMob.getLocation()) ||
            !player.hasLineOfSight(targetMob.getLocation()))
        {
            _targetTileOffsetX = -1;  //target cursor not set or not on screen
        } else {
            _targetTileOffsetX = targetMob.getX() - player.getX() + _widthInTiles/2;
            _targetTileOffsetY = targetMob.getY() - player.getY() + _heightInTiles/2;
        }

        
    }

    public boolean mouseMoved(int newx, int newy)
    {
        int newMouseDirection = getMouseWalkDirection(newx, newy);
        _cursorTileOffsetX = getTileByPixelX(newx);
        _cursorTileOffsetY = getTileByPixelY(newy);
        
        if (!_meleeCooldownCursorShown) {
            setMouseCursor(newMouseDirection, _currentMouseWalkDirection);
        }
        
        _currentMouseWalkDirection = newMouseDirection;
        
        return false;
    }
    
    public Location getTargetLocation()
    {
        if (_cursorTileOffsetX == -1) {
            return null;
        }
        return new Location(
                getMapByTileX(_cursorTileOffsetX),
                getMapByTileY(_cursorTileOffsetY),
                _frontend.getPlayerCharacter().getMap()
        );
    }

    /*
    public boolean mouseMoved(int newx, int newy)
    {
        _cursorTileOffsetX = getTileByPixelX(newx);
        _cursorTileOffsetY = getTileByPixelY(newy);
        
        int newMouseDirection = getMouseWalkDirection(
                getMapByTileX(_cursorTileOffsetX),
                getMapByTileY(_cursorTileOffsetY)
        );
        
        if (newMouseDirection != _currentMouseWalkDirection) {
            setMouseWalkCursor(newMouseDirection);
        }
        
        _currentMouseWalkDirection = newMouseDirection;
        
        return false;
    }*/
    
    private void setMouseCursor(int dir, int oldDir)
    {
        PlayerCharacter player = _frontend.getPlayerCharacter();

        int mapX = getMapByTileX(_cursorTileOffsetX);
        int mapY = getMapByTileY(_cursorTileOffsetY);
        Mobile mobile = player.getMap().getMobile(mapX, mapY);
        
        if (mobile != null && mobile.isHostileTo(player))
        {
            getUIManager().setMouseCursor(SlickResources.CURSOR_SWORD);
            _meleeCursorShown = true;
            return;
        } else {
            if (_meleeCursorShown) {
                oldDir = -1;
            }
            _meleeCursorShown = false;
        }
        
        if (dir == oldDir) {
            return;
        }

        switch (dir)
        {
            case Direction.NORTH:
                getUIManager().setMouseCursor(SlickResources.CURSOR_N);
                break;
            case Direction.SOUTH:
                getUIManager().setMouseCursor(SlickResources.CURSOR_S);
                break;
            case Direction.EAST:
                getUIManager().setMouseCursor(SlickResources.CURSOR_E);
                break;
            case Direction.WEST:
                getUIManager().setMouseCursor(SlickResources.CURSOR_W);
                break;
            case Direction.NORTH|Direction.EAST:
                getUIManager().setMouseCursor(SlickResources.CURSOR_NE);
                break;
            case Direction.NORTH|Direction.WEST:
                getUIManager().setMouseCursor(SlickResources.CURSOR_NW);
                break;
            case Direction.SOUTH|Direction.EAST:
                getUIManager().setMouseCursor(SlickResources.CURSOR_SE);
                break;
            case Direction.SOUTH|Direction.WEST:
                getUIManager().setMouseCursor(SlickResources.CURSOR_SW);
                break;
            default:
        }
    }

    /*
    public void mouseLeft()
    {
    }
    */
    
    protected void mouseLeft(int newx, int newy)
    {
        _cursorTileOffsetX = -1; //out of screen
        _currentMouseWalkDirection = Direction.NOWHERE;
        
        if (UIManager.getCurrentlyDraggedItem() == null) {
            getUIManager().setMouseCursor(SlickResources.CURSOR_DEFAULT);
        }
    }


    public boolean mousePressed(int button, int x, int y)
    {
        //left button: set target
        if (button == 0)
        {
            if (_cursorTileOffsetX != -1) {
                _frontend.setTarget(
                        getMapByTileX(_cursorTileOffsetX),
                        getMapByTileY(_cursorTileOffsetY)
                );
            }
            
        //right button: walk and use items
        } else if (button == 1) {
            
            boolean didMelee =
                _frontend.playerMelee(_currentMouseWalkDirection, true);
            
            if (!didMelee) {
                _mouseDownForWalking = true;
            } else {
                //change to melee cursor?
                PlayerCharacter player = _frontend.getPlayerCharacter();
                if (player.getMeleeCooldownTimeLeft() > 0) {
                    _meleeCooldownCursorShown = true;
                    getUIManager().setMouseCursor(_frontend.getFightingCursor());
                }
            }
            
        }
        return true;
    }
    
    /**
     * On dclick use items on ground.
     */
    protected boolean mouseDoubleClicked(int button, int x, int y)
    {
        PlayerCharacter player = _frontend.getPlayerCharacter();
        if (_cursorTileOffsetX == -1) {
            return false;
        }
        
        Location clickedLoc = new Location(
                getMapByTileX(_cursorTileOffsetX),
                getMapByTileY(_cursorTileOffsetY),
                player.getMap()
        );
        
        /*
        //on self dclick, open inventory:
        if (clickedLoc.equals(player.getLocation())) {
            _frontend.toggleInventory();
            return true;
        }
        */
        
        List items = clickedLoc.getItems();
        
        if (items == null) {
            return false;
        }
        
        for (int i = 0; i < items.size(); i++)
        {
            Item item = (Item)items.get(i);
            if (player.canUseItem(item)) {
                ((Activatable)item).activate(player);
                return true;
            }
        }

        return false;
    }

    
    public boolean mouseReleased(int button, int x, int y)
    {
        if (button == 1) {
            _mouseDownForWalking = false;
        }
        return true;
    }
    
    public int getMouseWalkDirection(int mouseX, int mouseY)
    {
        int dx = mouseX - _componentWidth / 2;
        int dy = mouseY - _componentHeight / 2;
        int dxAbs = Math.abs(dx);
        int dyAbs = Math.abs(dy);

        if (dy < 0 && dyAbs >= 2*dxAbs) {
            return Direction.NORTH;
        } else if (dx < 0 && dy < 0 && dyAbs < 2*dxAbs  && dxAbs < 2*dyAbs ) {
            return Direction.NORTH|Direction.WEST;
        } else if (dx < 0           && dxAbs >= 2*dyAbs ) {
            return Direction.WEST;
        } else if (dx < 0 && dy >= 0 && dyAbs < 2*dxAbs  && dxAbs < 2*dyAbs ) {
            return Direction.SOUTH|Direction.WEST;
        } else if (          dy >= 0 && dyAbs >= 2*dxAbs) {
            return Direction.SOUTH;
        } else if (dx >= 0 && dy >= 0 && dyAbs < 2*dxAbs  && dxAbs < 2*dyAbs ) {
            return Direction.SOUTH|Direction.EAST;
        } else if (dx >= 0            && dxAbs >= 2*dyAbs ) {
            return Direction.EAST;
        } else if (dx >= 0 && dy < 0 && dyAbs < 2*dxAbs  && dxAbs < 2*dyAbs ) {
            return Direction.NORTH|Direction.EAST;
        } else {
            throw new RuntimeException("This should never happen");
        }
    }
    
    public String getTooltipText(int x, int y)
    {
        GameMap map = _frontend.getPlayerCharacter().getMap();

        int mapX = getMapByPixelX(x);
        int mapY = getMapByPixelY(y);

        Mobile mobUnderMouse = map.getMobile(mapX, mapY);
        if (mobUnderMouse != null) {
            return mobUnderMouse.getShortDescription();
        }
        
        List itemsUnderMouse = map.getItems(mapX, mapY);
        if (itemsUnderMouse != null && itemsUnderMouse.size() > 0)
        {
            String txt = ((Item)itemsUnderMouse.get(itemsUnderMouse.size()-1)).
                         getShortDescription();
            if (itemsUnderMouse.size() > 1) {
                txt += " and more";
            }
            return txt;
        }
        
        return null;
    }
    
    

    /*
     * Drag and Drop stuff.
     */

    
    public DraggeableItem getDraggeableItem(int x, int y)
    {
        PlayerCharacter player = _frontend.getPlayerCharacter(); 
        int mapX = getMapByPixelX(x);
        int mapY = getMapByPixelY(y);
        
        //drag first item that is not locked down.
        List items = player.getMap().getItems(mapX, mapY);
        if (items != null && items.size() > 0)
        {
            for (int i = 0; i < items.size(); i++)
            {
                Item item = (Item)items.get(i);
                if (item.isLockedDown()) {
                    continue;
                }
                if (item.getLocation().distance(player.getLocation()) > 1) {
                    player.infoMsg("Too far away.");
                    continue;
                }
                
                DraggeableItem icon = SlickFrontend.createMenuItemFromEntity(item);
                icon.setOwnerComponent(this);

                return icon;
            }
        }
        return null;
    }

    /**
     * Several possibilities here:
     * We accept only _items_ from the inventory or from this component.
     * - Items dropped onto the player are picked up if they come from the map.
     * - Items dropped 1 field away from the player, are dropped on that field,
     *   if they come from the map or the inventory and the field is not blocked.
     * - Items dropped elsewhere are dropped to the player location
     *   if they come from the inventory.
     *   
     *   TODO: give stuff to other people by dropping it onto them.
     */
    protected boolean tryDropItem(DraggeableItem droppedItem, int x, int y)
    {
        PlayerCharacter player = _frontend.getPlayerCharacter();
        
        Object droppedObj = droppedItem.getReferencedObject();

        //not an item?
        if (!(droppedObj instanceof Item)) {
            return false;
        }
        
        Item item = (Item)droppedObj;
        int mapX = getMapByPixelX(x);
        int mapY = getMapByPixelY(y);


        int distance = (int)Coordinates.euclideanDistance(
                mapX, mapY, player.getX(), player.getY()
        );

        //dropped onto self?
        if (distance == 0)
        {
            //pickup?
            if (player.tryPickup(item)) {
                return true;
            }
        }
            
        Location dropLoc;
        if (distance < 2) //dropped 1 tile away?
        {
            dropLoc = new Location(mapX, mapY, player.getMap());
        } else {
            dropLoc = player.getLocation();
        }
        
        if (dropLoc.isBlocked()) {
            return false;
        }
        
        item.moveTo(dropLoc);
        
        return true;
    }



    protected void itemDraggedAway(DraggeableItem draggedItem)
    {
        //everythig was already done by item#moveTo and co;
    }


    
    

    /**
     * Funny message over the heads of mobiles or items.
     */
    public void addOverheadMessage(Entity emitter, String message,
            Color color, Easing easing)
    {
        _overheadMessages.add(
                new OverheadMessage(emitter, message, color, easing)
        );
    }
    
    class OverheadMessage
    {
        long startTime;
        Entity emitter;
        String message;
        Color color;
        Font font = getFont();
        PropertyGlider glider;
        Vector2f posMod;
        boolean finished = false;
        
        OverheadMessage(Entity emitter, String message,
                Color color, Easing easing)
        {
            this.color = new Color(color);
            this.emitter = emitter;
            this.message = message;
            this.startTime = System.currentTimeMillis();
            
            //begin centered over emitter's head.
            posMod = new Vector2f(
                    -getFont().getWidth(message) / 2,
                    -MAPTILE_HEIGHT * _scale
            );
            //fly to UPPER screen border:
            glider = new VectorGlider(
                    posMod,
                    new Vector2f(posMod.x, posMod.y-60),
                    OVERHEAD_MSG_DECAY_TIME
            );
            glider.setEasing(easing);
                    
        }
        
        void update(int delta)
        {
            glider.update(delta);
            
            int timeElapsed = (int)(System.currentTimeMillis() - startTime);
            //remove it?
            //TODO: what if it got picked up or teleported away?
            if (emitter.getLocation() == null ||
                    timeElapsed > OVERHEAD_MSG_DECAY_TIME)
            {
                finished = true;
            } else {
                //fade color alpha:
                //TODO: ease more non-linear.
                color.a = 1f - (float)timeElapsed/OVERHEAD_MSG_DECAY_TIME;
            }

        }
        
        void render(Graphics g, int screenX, int screenY)
        {
            Vector2f vec =
                getWindowRelativePixelLocation(emitter.getLocation());
            
            //draw overhead:
            g.setFont(getFont());

            g.setColor(color);
            g.drawString(
                    message,
                    screenX + vec.x + posMod.x,
                    screenY + vec.y + posMod.y
            );
        }
    }

    

    
    /*
     * Particle effect stuff
     */

    private void updateParticleEffects(int delta)
    {
        List finishedEffects = null;
        
        for (Iterator it = _effects.iterator(); it.hasNext(); )
        {
            AbstractSlickGameEffect effect = (AbstractSlickGameEffect)it.next();
            boolean effectActive = false;
            try {
                effect.update(delta);
                effectActive = effect.isActive(); 
            } catch (SlickException e) {
                Log.error("Failed rendering effect: " + e);
            }
            if (!effectActive) {
                if (finishedEffects == null) {
                    finishedEffects = new LinkedList();
                }
                finishedEffects.add(effect);
            }
        }

        if (finishedEffects != null) {
            _effects.removeAll(finishedEffects);
        }
        
        if (!_newEffects.isEmpty())
        {
            _effects.addAll(_newEffects);
            _newEffects.clear();
        }
    }
    
    public void addEffect(GameEffect effectConfig)
    {
        try {
            
            AbstractSlickGameEffect gameEffect;
            
            switch (effectConfig.getType())
            {
                case GameEffect.ENTITY_BOUND:
                    gameEffect = new EntityBoundEffect(effectConfig);
                    break;
                case GameEffect.CHASING:
                    gameEffect = new ChasingEffect(effectConfig);
                    break;
                case GameEffect.FLYING:
                    gameEffect = new FlyingEffect(effectConfig);
                    break;
                case GameEffect.STATIONARY:
                    gameEffect = new StationaryEffect(effectConfig);
                    break;
                case GameEffect.PROJECTILE:
                    gameEffect = new ProjectileEffect(effectConfig);
                    break;
                default:
                    throw new RuntimeException("Unhandled Effect Type: "
                            + effectConfig.getType());
            }
            
            _newEffects.add(gameEffect);
        
        } catch (SlickException e) {
            Log.error(
                    "Unable to create particle effect for dungeon window: " + e
            );
        }
    }


    class StationaryEffect extends ParticleGameEffect
    {

        StationaryEffect(GameEffect config) throws SlickException
        {
            super(config);
        }

        public void updateEffect(int delta)
        {
            Vector2f locVec = getWindowRelativePixelLocation(
                    config.getOrigin()
            );
            setX(locVec.x);
            setY(locVec.y);
        }
    }

    class EntityBoundEffect extends ParticleGameEffect
    {

        EntityBoundEffect(GameEffect config) throws SlickException
        {
            super(config);
        }

        public void updateEffect(int delta)
        {
            Vector2f locVec = getWindowRelativePixelLocation(
                    config.getEntity().getLocation()
            );
            setX(locVec.x);
            setY(locVec.y);
        }
    }
    
    
    class FlyingEffect extends ParticleGameEffect
    {
        //ConfigurableEmitter _emitter = null;

        FlyingEffect(GameEffect config) throws SlickException
        {
            super(config);
            currMapVec = getMapRelativePixelLocation(config.getOrigin());
            targMapVec = getMapRelativePixelLocation(config.getTargetLocation());

            updateMotion();
        }
        
        public void updateEffect(int delta)
        {
            //move me:
            currMapVec.x += motion.x;
            currMapVec.y += motion.y;

            Vector2f windowRelative = getWindowRelativePixelLocation(currMapVec);
            setX(windowRelative.x);
            setY(windowRelative.y);

            //target reached or hit something blocking? then notify observer
            Location currLoc = getLocationByMapRelativePixel(currMapVec);
            config.setCurrentLocation(currLoc);
        }
    }
 
    
    
    class ChasingEffect extends FlyingEffect
    {
        int _targetXOld;
        int _targetYOld;

        ChasingEffect(GameEffect config) throws SlickException
        {
            super(config);
            _targetXOld = config.getTargetEntity().getX();
            _targetYOld = config.getTargetEntity().getY();
        }
        
        public void updateEffect(int delta)
        {
            if (targetChangedLocation()) {
                updateMotion();
            }

            super.updateEffect(delta);
        }
        
        protected boolean targetChangedLocation()
        {
            Entity target = config.getTargetEntity();
            if (_targetXOld != target.getX() || _targetYOld != target.getY())
            {
                _targetXOld = target.getX();
                _targetYOld = target.getY();
                return true;
            }
            return false;
        }
    }
    
    
    
    class ProjectileEffect extends TileGameEffect
    {

        ProjectileEffect(GameEffect config) throws SlickException
        {
            super(config);
            currMapVec = getMapRelativePixelLocation(config.getOrigin());
            targMapVec = getMapRelativePixelLocation(config.getTargetLocation());
            updateMotion();
        }
        
        public void updateEffect(int delta)
        {
            //move me:
            currMapVec.x += motion.x;
            currMapVec.y += motion.y;

            Vector2f windowRelative = getWindowRelativePixelLocation(currMapVec);
            setX(windowRelative.x);
            setY(windowRelative.y);
            
            Location currLoc = getLocationByMapRelativePixel(currMapVec);
            config.setCurrentLocation(currLoc);
        }
    }


}
